/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.generator;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static javax.xml.xpath.XPathConstants.NODESET;

/**
 * XML utilities in specific use for handling the model files.
 *
 * @author Nanjing RedOrange (http://www.red-orange.cn)
 */
public class XMLUtil {

    /**
     * Transform xml Node with xslt template file to stream
     */
    public static void xsltTransform(@NotNull Node xml, @NotNull InputStream xslt, @NotNull Writer out
                                    , @NotNull Map<String, String> param) throws TransformerException {

        Source xmlSource = new DOMSource(xml);
        Source xsltSource = new StreamSource(xslt);
        Transformer transformer = TransformerFactory.newInstance().newTransformer(xsltSource);

        for (String key : param.keySet()) {
            transformer.setParameter(key, param.get(key));
        }

        transformer.transform(xmlSource, new StreamResult(out));
    }

    /**
     * Retrieves the distinct values of all 'language' attributes in given
     * document.
     *
     * @param doc (Mandatory) The AllModels document
     * @return Empty or filled set.
     * @throws ModelGenerationException Wrapper on XPath exceptions
     */
    @NotNull
    public static Set<String> definedLanguages(@NotNull Document doc) throws ModelGenerationException {
        Set<String> retVal = new HashSet<String>();

        XPath xpath = XPathFactory.newInstance().newXPath();
        NodeList langNodes;
        try {
            langNodes = (NodeList) xpath.evaluate("//@language", doc, NODESET);
        } catch (XPathExpressionException e) {
            throw new ModelGenerationException("Filter on language failed", e);
        }
        if (langNodes != null) {
            for (int i = 0; i < langNodes.getLength(); i++) {
                retVal.add(langNodes.item(i).getNodeValue());
            }
            System.out.println("Found " + retVal.size()
                    + " different languages.");
        } else {
            System.out.println("No captions found, the client will be using the model attribute names for caption.");
        }

        return retVal;
    }

    /**
     * Looks if this model document has the sorted = true tag.
     * @param doc (Mandatory) A model document
     * @return True if yes
     * @throws XPathExpressionException Just passing on
     */
    public static boolean isSortable(@NotNull Document doc) throws XPathExpressionException {
        XPath xpath = XPathFactory.newInstance().newXPath();
        NodeList sorted = (NodeList)xpath.evaluate("/object/sorted", doc, NODESET);
        return (sorted.getLength() == 1 && sorted.item(0).getFirstChild().getNodeValue().equals("true"));
    }

    /**
     * Return a nodelist with all the /object/attributes, or filtered as you xpath wished.
     *
     * @param doc (Mandatory) A model document
     * @param filter (Optional) XPathish filter. Default it is '*' (all).
     * @return A list of attributes
     * @throws XPathExpressionException Just passing on
     */
    @NotNull
    public static NodeList attributes(@NotNull Document doc, @Nullable String filter) throws XPathExpressionException {
        XPath xpath = XPathFactory.newInstance().newXPath();
        if (filter == null) {
            filter = "*";
        }
        return (NodeList)xpath.evaluate("/object/attributes/" + filter, doc, NODESET);
    }

    public static NodeList queries(@NotNull Document doc) throws XPathExpressionException {
        XPath xpath = XPathFactory.newInstance().newXPath();
        return (NodeList)xpath.evaluate("/object/queries/query", doc, NODESET);
    }

    /**
     * Insert attribute in given node. If this attribute already exists, the new attribute will not be inserted
     *
     * @param node  (Mandatory) Node in need of attribute
     * @param name  (Mandatory) Attribute name
     * @param value (Mandatory) It's value
     */
    public static void attribute(@NotNull Node node, @NotNull String name, @NotNull String value) {
        if (node.getAttributes().getNamedItem(name) == null) {
            Attr att = node.getOwnerDocument().createAttribute(name);
            att.setNodeValue(value);
            node.getAttributes().setNamedItem(att);
        }
    }

    @Nullable
    public static Node finder(@NotNull Node parent, @Nullable String name) {
        NodeList finders = parent.getChildNodes();
        for (int f = 0; f < finders.getLength(); f++)
            if ("finder".equals(finders.item(f).getNodeName()) &&
                    (name == null || name.equals(finders.item(f).getAttributes().getNamedItem("name").getNodeValue())))
                return finders.item(f);
        return null;
    }

    @NotNull
    public static Node addFinder(@NotNull Document doc, @NotNull Node parent, String jsonValue, boolean forceCreate) {
        Node retVal = finder(parent, null);
        if (forceCreate || retVal == null) {
            retVal = doc.createElement("finder");
            parent.appendChild(retVal);
        }
        attribute(retVal, "json", jsonValue);

        return retVal;
    }

    @NotNull
    public static Node addFinder(@NotNull Document doc, @NotNull Node parent, String name, String jsonValue) {
        Node retVal = finder(parent, name);
        if (retVal == null) {
            retVal = doc.createElement("finder");
            parent.appendChild(retVal);
        }
        attribute(retVal, "json", jsonValue);
        attribute(retVal, "name", name);

        return retVal;
    }

    /**
     * Adds an child element to the document's root element. Only adds the tag
     * if it not yet already exists.
     *
     * @param doc   (Mandatory) The XML document
     * @param tag   (Mandatory) The element's name
     * @param value (Optional) It's value, when null, the tag will be empty
     */
    public static void addChildElement(@NotNull Document doc, @NotNull String tag, @Nullable String value) {
        if (doc.getElementsByTagName(tag).getLength() == 0) {
            Node tagNode = doc.createElement(tag);
            if (value != null) {
                tagNode.setTextContent(value);
            }
            doc.getFirstChild().appendChild(tagNode);
        }
    }

    /**
     * Retrieves all enum from model doc.
     *
     * @param allModels   (Mandatory) The allModels document
     * @param basePackage (Mandatory)
     * @return Empty or filled set.
     * @throws ModelGenerationException Wrapper on XPath exceptions
     */
    @NotNull
    public static Map<String, Document> enumerations(@NotNull Document allModels, @NotNull String basePackage)
            throws ModelGenerationException {
        Map<String, Document> retVal = new HashMap<String, Document>();

        NodeList enumNodes;
        try {
            enumNodes = (NodeList) XPathFactory.newInstance().newXPath().evaluate("//enum[@scope='global']"
                    , allModels, NODESET);
        } catch (XPathExpressionException e) {
            throw new ModelGenerationException("The search on global scoped enums failed", e);
        }
        if (enumNodes != null) {
            for (int i = 0; i < enumNodes.getLength(); i++) {
                String name = enumNodes.item(i).getAttributes().getNamedItem("class").getNodeValue();
                Document enumDoc = newDocument(basePackage, "enums");
                addChildElement(enumDoc, "name", name);
                addChildElement(enumDoc, "fieldName", enumNodes.item(i).getAttributes().getNamedItem("fieldName").getNodeValue());
                enumDoc.getFirstChild().appendChild(enumDoc.importNode(enumNodes.item(i), true));
                retVal.put(name, enumDoc);
            }
        } else {
            System.out.println("No captions found, the client will be using the model attribute names for caption.");
        }

        return retVal;
    }

    /**
     * Create the model XML document and adds the root element and optional a
     * package element. This method is simple and created for your convenience.
     *
     * @param basePackage (Optional) Adds a 'package' element, it will the the root's
     *                    first child
     * @param root        (Mandatory) root element name
     * @return Document The Model document
     * @throws ModelGenerationException Wrapper on ParserConfigurationException: should not happen
     */
    @NotNull
    public static Document newDocument(@Nullable String basePackage, @NotNull String root)
            throws ModelGenerationException {
        Document doc;

        try {
            doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        } catch (ParserConfigurationException e) {
            throw new ModelGenerationException(
                    "Strange, the DOM is not configured right, it worked before! Maybe you have some system properties set that interfere with me",
                    e);
        }

        Node insertNode = doc.createElement(root);
        doc.appendChild(doc.importNode(insertNode, true));
        if (basePackage != null) {
            addChildElement(doc, "package", basePackage);
        }
        return doc;
    }

    /**
     * Dumps Document to String. Any exceptions are ignored
     *
     * @param doc (Mandatory)
     * @return String of given doc.
     */
    @NotNull
    public static String asString(@NotNull Document doc) {
        Source source = new DOMSource(doc);
        OutputStream out = new ByteArrayOutputStream();
        Result result = new StreamResult(out);
        try {
            TransformerFactory.newInstance().newTransformer().transform(source, result);
        } catch (TransformerException e) {
            //Ignoring error, this is in error handling already
            e.printStackTrace();
        }
        return out.toString();
    }
}
